|
1
|
|
|
import $ from 'jquery'; |
|
|
|
|
|
|
2
|
|
|
import _ from 'underscore'; |
|
3
|
|
|
import { |
|
4
|
|
|
Backbone, |
|
5
|
|
|
wrapError, |
|
6
|
|
|
addUnderscoreMethods |
|
7
|
|
|
} from './core.js'; |
|
8
|
|
|
import { |
|
9
|
|
|
Events |
|
10
|
|
|
} from './events.js'; |
|
11
|
|
|
import { |
|
12
|
|
|
Model |
|
13
|
|
|
} from './model.js'; |
|
14
|
|
|
|
|
15
|
|
|
// Create a local reference to a common array method we'll want to use later. |
|
16
|
|
|
var slice = Array.prototype.slice; |
|
17
|
|
|
|
|
18
|
|
|
// Backbone.Collection |
|
19
|
|
|
// ------------------- |
|
20
|
|
|
|
|
21
|
|
|
// If models tend to represent a single row of data, a Backbone Collection is |
|
22
|
|
|
// more analogous to a table full of data ... or a small slice or page of that |
|
23
|
|
|
// table, or a collection of rows that belong together for a particular reason |
|
24
|
|
|
// -- all of the messages in this particular folder, all of the documents |
|
25
|
|
|
// belonging to this particular author, and so on. Collections maintain |
|
26
|
|
|
// indexes of their models, both in order, and for lookup by `id`. |
|
27
|
|
|
|
|
28
|
|
|
// Create a new **Collection**, perhaps to contain a specific type of `model`. |
|
29
|
|
|
// If a `comparator` is specified, the Collection will maintain |
|
30
|
|
|
// its models in sort order, as they're added and removed. |
|
31
|
|
|
var Collection = function (models, options) { |
|
32
|
|
|
options = options || {}; |
|
33
|
|
|
this.preinitialize.apply(this, arguments); |
|
34
|
|
|
if (options.model) { |
|
35
|
|
|
this.model = options.model; |
|
36
|
|
|
} |
|
37
|
|
|
if (options.comparator !== void 0) { |
|
38
|
|
|
this.comparator = options.comparator; |
|
39
|
|
|
} |
|
40
|
|
|
this._reset(); |
|
41
|
|
|
this.initialize.apply(this, arguments); |
|
42
|
|
|
if (models) { |
|
43
|
|
|
this.reset(models, _.extend({ |
|
44
|
|
|
silent: true |
|
45
|
|
|
}, options)); |
|
46
|
|
|
} |
|
47
|
|
|
}; |
|
48
|
|
|
|
|
49
|
|
|
// Default options for `Collection#set`. |
|
50
|
|
|
var setOptions = { |
|
51
|
|
|
add: true, |
|
52
|
|
|
remove: true, |
|
53
|
|
|
merge: true |
|
54
|
|
|
}; |
|
55
|
|
|
var addOptions = { |
|
56
|
|
|
add: true, |
|
57
|
|
|
remove: false |
|
58
|
|
|
}; |
|
59
|
|
|
|
|
60
|
|
|
// Splices `insert` into `array` at index `at`. |
|
61
|
|
|
var splice = function (array, insert, at) { |
|
62
|
|
|
at = Math.min(Math.max(at, 0), array.length); |
|
63
|
|
|
var tail = Array(array.length - at); |
|
64
|
|
|
var length = insert.length; |
|
65
|
|
|
var i; |
|
66
|
|
|
for (i = 0; i < tail.length; i++) { |
|
67
|
|
|
tail[i] = array[i + at]; |
|
68
|
|
|
} |
|
69
|
|
|
for (i = 0; i < length; i++) { |
|
70
|
|
|
array[i + at] = insert[i]; |
|
71
|
|
|
} |
|
72
|
|
|
for (i = 0; i < tail.length; i++) { |
|
73
|
|
|
array[i + length + at] = tail[i]; |
|
74
|
|
|
} |
|
75
|
|
|
}; |
|
76
|
|
|
|
|
77
|
|
|
// Define the Collection's inheritable methods. |
|
78
|
|
|
_.extend(Collection.prototype, Events, { |
|
79
|
|
|
|
|
80
|
|
|
// The default model for a collection is just a **Backbone.Model**. |
|
81
|
|
|
// This should be overridden in most cases. |
|
82
|
|
|
model: Model, |
|
83
|
|
|
|
|
84
|
|
|
// preinitialize is an empty function by default. You can override it with a function |
|
85
|
|
|
// or object. preinitialize will run before any instantiation logic is run in the Collection. |
|
86
|
|
|
preinitialize: function () {}, |
|
87
|
|
|
|
|
88
|
|
|
// Initialize is an empty function by default. Override it with your own |
|
89
|
|
|
// initialization logic. |
|
90
|
|
|
initialize: function () {}, |
|
91
|
|
|
|
|
92
|
|
|
// The JSON representation of a Collection is an array of the |
|
93
|
|
|
// models' attributes. |
|
94
|
|
|
toJSON: function (options) { |
|
95
|
|
|
return this.map(function (model) { |
|
96
|
|
|
return model.toJSON(options); |
|
97
|
|
|
}); |
|
98
|
|
|
}, |
|
99
|
|
|
|
|
100
|
|
|
// Proxy `Backbone.sync` by default. |
|
101
|
|
|
sync: function () { |
|
102
|
|
|
return Backbone.sync.apply(this, arguments); |
|
103
|
|
|
}, |
|
104
|
|
|
|
|
105
|
|
|
// Add a model, or list of models to the set. `models` may be Backbone |
|
106
|
|
|
// Models or raw JavaScript objects to be converted to Models, or any |
|
107
|
|
|
// combination of the two. |
|
108
|
|
|
add: function (models, options) { |
|
109
|
|
|
return this.set(models, _.extend({ |
|
110
|
|
|
merge: false |
|
111
|
|
|
}, options, addOptions)); |
|
112
|
|
|
}, |
|
113
|
|
|
|
|
114
|
|
|
// Remove a model, or a list of models from the set. |
|
115
|
|
|
remove: function (models, options) { |
|
116
|
|
|
options = _.extend({}, options); |
|
117
|
|
|
var singular = !_.isArray(models); |
|
118
|
|
|
models = singular ? [models] : models.slice(); |
|
119
|
|
|
var removed = this._removeModels(models, options); |
|
120
|
|
|
if (!options.silent && removed.length) { |
|
121
|
|
|
options.changes = { |
|
122
|
|
|
added: [], |
|
123
|
|
|
merged: [], |
|
124
|
|
|
removed: removed |
|
125
|
|
|
}; |
|
126
|
|
|
this.trigger('update', this, options); |
|
127
|
|
|
} |
|
128
|
|
|
return singular ? removed[0] : removed; |
|
129
|
|
|
}, |
|
130
|
|
|
|
|
131
|
|
|
// Update a collection by `set`-ing a new list of models, adding new ones, |
|
132
|
|
|
// removing models that are no longer present, and merging models that |
|
133
|
|
|
// already exist in the collection, as necessary. Similar to **Model#set**, |
|
134
|
|
|
// the core operation for updating the data contained by the collection. |
|
135
|
|
|
set: function (models, options) { |
|
136
|
|
|
if (models == null) { |
|
137
|
|
|
return; |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
options = _.extend({}, setOptions, options); |
|
141
|
|
|
if (options.parse && !this._isModel(models)) { |
|
142
|
|
|
models = this.parse(models, options) || []; |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
var singular = !_.isArray(models); |
|
146
|
|
|
models = singular ? [models] : models.slice(); |
|
147
|
|
|
|
|
148
|
|
|
var at = options.at; |
|
149
|
|
|
if (at != null) { |
|
150
|
|
|
at = +at; |
|
151
|
|
|
} |
|
152
|
|
|
if (at > this.length) { |
|
153
|
|
|
at = this.length; |
|
154
|
|
|
} |
|
155
|
|
|
if (at < 0) { |
|
156
|
|
|
at += this.length + 1; |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
var set = []; |
|
160
|
|
|
var toAdd = []; |
|
161
|
|
|
var toMerge = []; |
|
162
|
|
|
var toRemove = []; |
|
163
|
|
|
var modelMap = {}; |
|
164
|
|
|
|
|
165
|
|
|
var add = options.add; |
|
166
|
|
|
var merge = options.merge; |
|
167
|
|
|
var remove = options.remove; |
|
168
|
|
|
|
|
169
|
|
|
var sort = false; |
|
170
|
|
|
var sortable = this.comparator && at == null && options.sort !== |
|
171
|
|
|
false; |
|
172
|
|
|
var sortAttr = _.isString(this.comparator) ? this.comparator : |
|
173
|
|
|
null; |
|
174
|
|
|
|
|
175
|
|
|
// Turn bare objects into model references, and prevent invalid models |
|
176
|
|
|
// from being added. |
|
177
|
|
|
var model, i; |
|
178
|
|
|
for (i = 0; i < models.length; i++) { |
|
179
|
|
|
model = models[i]; |
|
180
|
|
|
|
|
181
|
|
|
// If a duplicate is found, prevent it from being added and |
|
182
|
|
|
// optionally merge it into the existing model. |
|
183
|
|
|
var existing = this.get(model); |
|
184
|
|
|
if (existing) { |
|
185
|
|
|
if (merge && model !== existing) { |
|
186
|
|
|
var attrs = this._isModel(model) ? model.attributes : |
|
187
|
|
|
model; |
|
188
|
|
|
if (options.parse) { |
|
|
|
|
|
|
189
|
|
|
attrs = existing.parse(attrs, |
|
190
|
|
|
options); |
|
191
|
|
|
} |
|
192
|
|
|
existing.set(attrs, options); |
|
193
|
|
|
toMerge.push(existing); |
|
194
|
|
|
if (sortable && !sort) { |
|
|
|
|
|
|
195
|
|
|
sort = existing.hasChanged( |
|
196
|
|
|
sortAttr); |
|
197
|
|
|
} |
|
198
|
|
|
} |
|
199
|
|
|
if (!modelMap[existing.cid]) { |
|
200
|
|
|
modelMap[existing.cid] = true; |
|
201
|
|
|
set.push(existing); |
|
202
|
|
|
} |
|
203
|
|
|
models[i] = existing; |
|
204
|
|
|
|
|
205
|
|
|
// If this is a new, valid model, push it to the `toAdd` list. |
|
206
|
|
|
} else if (add) { |
|
207
|
|
|
model = models[i] = this._prepareModel(model, options); |
|
208
|
|
|
if (model) { |
|
209
|
|
|
toAdd.push(model); |
|
210
|
|
|
this._addReference(model, options); |
|
211
|
|
|
modelMap[model.cid] = true; |
|
212
|
|
|
set.push(model); |
|
213
|
|
|
} |
|
214
|
|
|
} |
|
215
|
|
|
} |
|
216
|
|
|
|
|
217
|
|
|
// Remove stale models. |
|
218
|
|
|
if (remove) { |
|
219
|
|
|
for (i = 0; i < this.length; i++) { |
|
220
|
|
|
model = this.models[i]; |
|
221
|
|
|
if (!modelMap[model.cid]) { |
|
222
|
|
|
toRemove.push(model); |
|
223
|
|
|
} |
|
224
|
|
|
} |
|
225
|
|
|
if (toRemove.length) { |
|
226
|
|
|
this._removeModels(toRemove, options); |
|
227
|
|
|
} |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
// See if sorting is needed, update `length` and splice in new models. |
|
231
|
|
|
var orderChanged = false; |
|
232
|
|
|
var replace = !sortable && add && remove; |
|
233
|
|
|
if (set.length && replace) { |
|
234
|
|
|
orderChanged = this.length !== set.length || _.some(this.models, |
|
235
|
|
|
function (m, index) { |
|
236
|
|
|
return m !== set[index]; |
|
237
|
|
|
}); |
|
238
|
|
|
this.models.length = 0; |
|
239
|
|
|
splice(this.models, set, 0); |
|
240
|
|
|
this.length = this.models.length; |
|
241
|
|
|
} else if (toAdd.length) { |
|
242
|
|
|
if (sortable) { |
|
243
|
|
|
sort = true; |
|
244
|
|
|
} |
|
245
|
|
|
splice(this.models, toAdd, at == null ? this.length : at); |
|
246
|
|
|
this.length = this.models.length; |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
// Silently sort the collection if appropriate. |
|
250
|
|
|
if (sort) { |
|
251
|
|
|
this.sort({ |
|
252
|
|
|
silent: true |
|
253
|
|
|
}); |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
// Unless silenced, it's time to fire all appropriate add/sort/update events. |
|
257
|
|
|
if (!options.silent) { |
|
258
|
|
|
for (i = 0; i < toAdd.length; i++) { |
|
259
|
|
|
if (at != null) { |
|
260
|
|
|
options.index = at + i; |
|
261
|
|
|
} |
|
262
|
|
|
model = toAdd[i]; |
|
263
|
|
|
model.trigger('add', model, this, options); |
|
264
|
|
|
} |
|
265
|
|
|
if (sort || orderChanged) { |
|
266
|
|
|
this.trigger('sort', this, |
|
267
|
|
|
options); |
|
268
|
|
|
} |
|
269
|
|
|
if (toAdd.length || toRemove.length || toMerge.length) { |
|
270
|
|
|
options.changes = { |
|
271
|
|
|
added: toAdd, |
|
272
|
|
|
removed: toRemove, |
|
273
|
|
|
merged: toMerge |
|
274
|
|
|
}; |
|
275
|
|
|
this.trigger('update', this, options); |
|
276
|
|
|
} |
|
277
|
|
|
} |
|
278
|
|
|
|
|
279
|
|
|
// Return the added (or merged) model (or models). |
|
280
|
|
|
return singular ? models[0] : models; |
|
281
|
|
|
}, |
|
282
|
|
|
|
|
283
|
|
|
// When you have more items than you want to add or remove individually, |
|
284
|
|
|
// you can reset the entire set with a new list of models, without firing |
|
285
|
|
|
// any granular `add` or `remove` events. Fires `reset` when finished. |
|
286
|
|
|
// Useful for bulk operations and optimizations. |
|
287
|
|
|
reset: function (models, options) { |
|
288
|
|
|
options = options ? _.clone(options) : {}; |
|
289
|
|
|
for (var i = 0; i < this.models.length; i++) { |
|
290
|
|
|
this._removeReference(this.models[i], options); |
|
291
|
|
|
} |
|
292
|
|
|
options.previousModels = this.models; |
|
293
|
|
|
this._reset(); |
|
294
|
|
|
models = this.add(models, _.extend({ |
|
295
|
|
|
silent: true |
|
296
|
|
|
}, options)); |
|
297
|
|
|
if (!options.silent) { |
|
298
|
|
|
this.trigger('reset', this, options); |
|
299
|
|
|
} |
|
300
|
|
|
return models; |
|
301
|
|
|
}, |
|
302
|
|
|
|
|
303
|
|
|
// Add a model to the end of the collection. |
|
304
|
|
|
push: function (model, options) { |
|
305
|
|
|
return this.add(model, _.extend({ |
|
306
|
|
|
at: this.length |
|
307
|
|
|
}, options)); |
|
308
|
|
|
}, |
|
309
|
|
|
|
|
310
|
|
|
// Remove a model from the end of the collection. |
|
311
|
|
|
pop: function (options) { |
|
312
|
|
|
var model = this.at(this.length - 1); |
|
313
|
|
|
return this.remove(model, options); |
|
314
|
|
|
}, |
|
315
|
|
|
|
|
316
|
|
|
// Add a model to the beginning of the collection. |
|
317
|
|
|
unshift: function (model, options) { |
|
318
|
|
|
return this.add(model, _.extend({ |
|
319
|
|
|
at: 0 |
|
320
|
|
|
}, options)); |
|
321
|
|
|
}, |
|
322
|
|
|
|
|
323
|
|
|
// Remove a model from the beginning of the collection. |
|
324
|
|
|
shift: function (options) { |
|
325
|
|
|
var model = this.at(0); |
|
326
|
|
|
return this.remove(model, options); |
|
327
|
|
|
}, |
|
328
|
|
|
|
|
329
|
|
|
// Slice out a sub-array of models from the collection. |
|
330
|
|
|
slice: function () { |
|
331
|
|
|
return slice.apply(this.models, arguments); |
|
332
|
|
|
}, |
|
333
|
|
|
|
|
334
|
|
|
// Get a model from the set by id, cid, model object with id or cid |
|
335
|
|
|
// properties, or an attributes object that is transformed through modelId. |
|
336
|
|
|
get: function (obj) { |
|
337
|
|
|
if (obj == null) { |
|
338
|
|
|
return void 0; |
|
339
|
|
|
} |
|
340
|
|
|
return this._byId[obj] || |
|
341
|
|
|
this._byId[this.modelId(obj.attributes || obj)] || |
|
342
|
|
|
obj.cid && this._byId[obj.cid]; |
|
343
|
|
|
}, |
|
344
|
|
|
|
|
345
|
|
|
// Returns `true` if the model is in the collection. |
|
346
|
|
|
has: function (obj) { |
|
347
|
|
|
return this.get(obj) != null; |
|
348
|
|
|
}, |
|
349
|
|
|
|
|
350
|
|
|
// Get the model at the given index. |
|
351
|
|
|
at: function (index) { |
|
352
|
|
|
if (index < 0) { |
|
353
|
|
|
index += this.length; |
|
354
|
|
|
} |
|
355
|
|
|
return this.models[index]; |
|
356
|
|
|
}, |
|
357
|
|
|
|
|
358
|
|
|
// Return models with matching attributes. Useful for simple cases of |
|
359
|
|
|
// `filter`. |
|
360
|
|
|
where: function (attrs, first) { |
|
361
|
|
|
return this[first ? 'find' : 'filter'](attrs); |
|
362
|
|
|
}, |
|
363
|
|
|
|
|
364
|
|
|
// Return the first model with matching attributes. Useful for simple cases |
|
365
|
|
|
// of `find`. |
|
366
|
|
|
findWhere: function (attrs) { |
|
367
|
|
|
return this.where(attrs, true); |
|
368
|
|
|
}, |
|
369
|
|
|
|
|
370
|
|
|
// Force the collection to re-sort itself. You don't need to call this under |
|
371
|
|
|
// normal circumstances, as the set will maintain sort order as each item |
|
372
|
|
|
// is added. |
|
373
|
|
|
sort: function (options) { |
|
374
|
|
|
var comparator = this.comparator; |
|
375
|
|
|
if (!comparator) { |
|
376
|
|
|
throw new Error( |
|
377
|
|
|
'Cannot sort a set without a comparator'); |
|
378
|
|
|
} |
|
379
|
|
|
options = options || {}; |
|
380
|
|
|
|
|
381
|
|
|
var length = comparator.length; |
|
382
|
|
|
if (_.isFunction(comparator)) { |
|
383
|
|
|
comparator = _.bind( |
|
384
|
|
|
comparator, |
|
385
|
|
|
this); |
|
386
|
|
|
} |
|
387
|
|
|
|
|
388
|
|
|
// Run sort based on type of `comparator`. |
|
389
|
|
|
if (length === 1 || _.isString(comparator)) { |
|
390
|
|
|
this.models = this.sortBy(comparator); |
|
391
|
|
|
} else { |
|
392
|
|
|
this.models.sort(comparator); |
|
393
|
|
|
} |
|
394
|
|
|
if (!options.silent) { |
|
395
|
|
|
this.trigger('sort', this, options); |
|
396
|
|
|
} |
|
397
|
|
|
return this; |
|
398
|
|
|
}, |
|
399
|
|
|
|
|
400
|
|
|
// Pluck an attribute from each model in the collection. |
|
401
|
|
|
pluck: function (attr) { |
|
402
|
|
|
return this.map(attr + ''); |
|
403
|
|
|
}, |
|
404
|
|
|
|
|
405
|
|
|
// Fetch the default set of models for this collection, resetting the |
|
406
|
|
|
// collection when they arrive. If `reset: true` is passed, the response |
|
407
|
|
|
// data will be passed through the `reset` method instead of `set`. |
|
408
|
|
|
fetch: function (options) { |
|
409
|
|
|
options = _.extend({ |
|
410
|
|
|
parse: true |
|
411
|
|
|
}, options); |
|
412
|
|
|
var success = options.success; |
|
413
|
|
|
var collection = this; |
|
|
|
|
|
|
414
|
|
|
options.success = function (resp) { |
|
415
|
|
|
var method = options.reset ? 'reset' : 'set'; |
|
416
|
|
|
collection[method](resp, options); |
|
417
|
|
|
if (success) { |
|
418
|
|
|
success.call(options.context, collection, |
|
419
|
|
|
resp, |
|
420
|
|
|
options); |
|
421
|
|
|
} |
|
422
|
|
|
collection.trigger('sync', collection, resp, options); |
|
423
|
|
|
}; |
|
424
|
|
|
wrapError(this, options); |
|
425
|
|
|
return this.sync('read', this, options); |
|
426
|
|
|
}, |
|
427
|
|
|
|
|
428
|
|
|
// Create a new instance of a model in this collection. Add the model to the |
|
429
|
|
|
// collection immediately, unless `wait: true` is passed, in which case we |
|
430
|
|
|
// wait for the server to agree. |
|
431
|
|
|
create: function (model, options) { |
|
432
|
|
|
options = options ? _.clone(options) : {}; |
|
433
|
|
|
var wait = options.wait; |
|
434
|
|
|
model = this._prepareModel(model, options); |
|
435
|
|
|
if (!model) { |
|
436
|
|
|
return false; |
|
437
|
|
|
} |
|
438
|
|
|
if (!wait) { |
|
439
|
|
|
this.add(model, options); |
|
440
|
|
|
} |
|
441
|
|
|
var collection = this; |
|
|
|
|
|
|
442
|
|
|
var success = options.success; |
|
443
|
|
|
options.success = function (m, resp, callbackOpts) { |
|
444
|
|
|
if (wait) { |
|
445
|
|
|
collection.add(m, callbackOpts); |
|
446
|
|
|
} |
|
447
|
|
|
if (success) { |
|
448
|
|
|
success.call(callbackOpts.context, m, resp, |
|
449
|
|
|
callbackOpts); |
|
450
|
|
|
} |
|
451
|
|
|
}; |
|
452
|
|
|
model.save(null, options); |
|
453
|
|
|
return model; |
|
454
|
|
|
}, |
|
455
|
|
|
|
|
456
|
|
|
// **parse** converts a response into a list of models to be added to the |
|
457
|
|
|
// collection. The default implementation is just to pass it through. |
|
458
|
|
|
parse: function (resp, options) { |
|
|
|
|
|
|
459
|
|
|
return resp; |
|
460
|
|
|
}, |
|
461
|
|
|
|
|
462
|
|
|
// Create a new collection with an identical list of models as this one. |
|
463
|
|
|
clone: function () { |
|
464
|
|
|
return new this.constructor(this.models, { |
|
465
|
|
|
model: this.model, |
|
466
|
|
|
comparator: this.comparator |
|
467
|
|
|
}); |
|
468
|
|
|
}, |
|
469
|
|
|
|
|
470
|
|
|
// Define how to uniquely identify models in the collection. |
|
471
|
|
|
modelId: function (attrs) { |
|
472
|
|
|
return attrs[this.model.prototype.idAttribute || 'id']; |
|
473
|
|
|
}, |
|
474
|
|
|
|
|
475
|
|
|
// Private method to reset all internal state. Called when the collection |
|
476
|
|
|
// is first initialized or reset. |
|
477
|
|
|
_reset: function () { |
|
478
|
|
|
this.length = 0; |
|
479
|
|
|
this.models = []; |
|
480
|
|
|
this._byId = {}; |
|
481
|
|
|
}, |
|
482
|
|
|
|
|
483
|
|
|
// Prepare a hash of attributes (or other model) to be added to this |
|
484
|
|
|
// collection. |
|
485
|
|
|
_prepareModel: function (attrs, options) { |
|
486
|
|
|
if (this._isModel(attrs)) { |
|
487
|
|
|
if (!attrs.collection) { |
|
488
|
|
|
attrs.collection = this; |
|
489
|
|
|
} |
|
490
|
|
|
return attrs; |
|
491
|
|
|
} |
|
492
|
|
|
options = options ? _.clone(options) : {}; |
|
493
|
|
|
options.collection = this; |
|
494
|
|
|
var model = new this.model(attrs, options); |
|
|
|
|
|
|
495
|
|
|
if (!model.validationError) { |
|
496
|
|
|
return model; |
|
497
|
|
|
} |
|
498
|
|
|
this.trigger('invalid', this, model.validationError, options); |
|
499
|
|
|
return false; |
|
500
|
|
|
}, |
|
501
|
|
|
|
|
502
|
|
|
// Internal method called by both remove and set. |
|
503
|
|
|
_removeModels: function (models, options) { |
|
504
|
|
|
var removed = []; |
|
505
|
|
|
for (var i = 0; i < models.length; i++) { |
|
506
|
|
|
var model = this.get(models[i]); |
|
507
|
|
|
if (!model) { |
|
508
|
|
|
continue; |
|
509
|
|
|
} |
|
510
|
|
|
|
|
511
|
|
|
var index = this.indexOf(model); |
|
512
|
|
|
this.models.splice(index, 1); |
|
513
|
|
|
this.length--; |
|
514
|
|
|
|
|
515
|
|
|
// Remove references before triggering 'remove' event to prevent an |
|
516
|
|
|
// infinite loop. #3693 |
|
517
|
|
|
delete this._byId[model.cid]; |
|
518
|
|
|
var id = this.modelId(model.attributes); |
|
519
|
|
|
if (id != null) { |
|
520
|
|
|
delete this._byId[id]; |
|
521
|
|
|
} |
|
522
|
|
|
|
|
523
|
|
|
if (!options.silent) { |
|
524
|
|
|
options.index = index; |
|
525
|
|
|
model.trigger('remove', model, this, options); |
|
526
|
|
|
} |
|
527
|
|
|
|
|
528
|
|
|
removed.push(model); |
|
529
|
|
|
this._removeReference(model, options); |
|
530
|
|
|
} |
|
531
|
|
|
return removed; |
|
532
|
|
|
}, |
|
533
|
|
|
|
|
534
|
|
|
// Method for checking whether an object should be considered a model for |
|
535
|
|
|
// the purposes of adding to the collection. |
|
536
|
|
|
_isModel: function (model) { |
|
537
|
|
|
return model instanceof Model; |
|
538
|
|
|
}, |
|
539
|
|
|
|
|
540
|
|
|
// Internal method to create a model's ties to a collection. |
|
541
|
|
|
_addReference: function (model, options) { |
|
|
|
|
|
|
542
|
|
|
this._byId[model.cid] = model; |
|
543
|
|
|
var id = this.modelId(model.attributes); |
|
544
|
|
|
if (id != null) { |
|
545
|
|
|
this._byId[id] = model; |
|
546
|
|
|
} |
|
547
|
|
|
model.on('all', this._onModelEvent, this); |
|
548
|
|
|
}, |
|
549
|
|
|
|
|
550
|
|
|
// Internal method to sever a model's ties to a collection. |
|
551
|
|
|
_removeReference: function (model, options) { |
|
|
|
|
|
|
552
|
|
|
delete this._byId[model.cid]; |
|
553
|
|
|
var id = this.modelId(model.attributes); |
|
554
|
|
|
if (id != null) { |
|
555
|
|
|
delete this._byId[id]; |
|
556
|
|
|
} |
|
557
|
|
|
if (this === model.collection) { |
|
558
|
|
|
delete model.collection; |
|
559
|
|
|
} |
|
560
|
|
|
model.off('all', this._onModelEvent, this); |
|
561
|
|
|
}, |
|
562
|
|
|
|
|
563
|
|
|
// Internal method called every time a model in the set fires an event. |
|
564
|
|
|
// Sets need to update their indexes when models change ids. All other |
|
565
|
|
|
// events simply proxy through. "add" and "remove" events that originate |
|
566
|
|
|
// in other collections are ignored. |
|
567
|
|
|
_onModelEvent: function (event, model, collection, options) { |
|
568
|
|
|
if (model) { |
|
569
|
|
|
if ((event === 'add' || event === 'remove') && collection !== |
|
570
|
|
|
this) { |
|
571
|
|
|
return; |
|
572
|
|
|
} |
|
573
|
|
|
if (event === 'destroy') { |
|
574
|
|
|
this.remove(model, options); |
|
575
|
|
|
} |
|
576
|
|
|
if (event === 'change') { |
|
577
|
|
|
var prevId = this.modelId(model.previousAttributes()); |
|
578
|
|
|
var id = this.modelId(model.attributes); |
|
579
|
|
|
if (prevId !== id) { |
|
580
|
|
|
if (prevId != null) { |
|
|
|
|
|
|
581
|
|
|
delete this._byId[prevId]; |
|
582
|
|
|
} |
|
583
|
|
|
if (id != null) { |
|
|
|
|
|
|
584
|
|
|
this._byId[id] = model; |
|
585
|
|
|
} |
|
586
|
|
|
} |
|
587
|
|
|
} |
|
588
|
|
|
} |
|
589
|
|
|
this.trigger.apply(this, arguments); |
|
590
|
|
|
} |
|
591
|
|
|
|
|
592
|
|
|
}); |
|
593
|
|
|
|
|
594
|
|
|
// Underscore methods that we want to implement on the Collection. |
|
595
|
|
|
// 90% of the core usefulness of Backbone Collections is actually implemented |
|
596
|
|
|
// right here: |
|
597
|
|
|
var collectionMethods = { |
|
598
|
|
|
forEach: 3, |
|
599
|
|
|
each: 3, |
|
600
|
|
|
map: 3, |
|
601
|
|
|
collect: 3, |
|
602
|
|
|
reduce: 0, |
|
603
|
|
|
foldl: 0, |
|
604
|
|
|
inject: 0, |
|
605
|
|
|
reduceRight: 0, |
|
606
|
|
|
foldr: 0, |
|
607
|
|
|
find: 3, |
|
608
|
|
|
detect: 3, |
|
609
|
|
|
filter: 3, |
|
610
|
|
|
select: 3, |
|
611
|
|
|
reject: 3, |
|
612
|
|
|
every: 3, |
|
613
|
|
|
all: 3, |
|
614
|
|
|
some: 3, |
|
615
|
|
|
any: 3, |
|
616
|
|
|
include: 3, |
|
617
|
|
|
includes: 3, |
|
618
|
|
|
contains: 3, |
|
619
|
|
|
invoke: 0, |
|
620
|
|
|
max: 3, |
|
621
|
|
|
min: 3, |
|
622
|
|
|
toArray: 1, |
|
623
|
|
|
size: 1, |
|
624
|
|
|
first: 3, |
|
625
|
|
|
head: 3, |
|
626
|
|
|
take: 3, |
|
627
|
|
|
initial: 3, |
|
628
|
|
|
rest: 3, |
|
629
|
|
|
tail: 3, |
|
630
|
|
|
drop: 3, |
|
631
|
|
|
last: 3, |
|
632
|
|
|
without: 0, |
|
633
|
|
|
difference: 0, |
|
634
|
|
|
indexOf: 3, |
|
635
|
|
|
shuffle: 1, |
|
636
|
|
|
lastIndexOf: 3, |
|
637
|
|
|
isEmpty: 1, |
|
638
|
|
|
chain: 1, |
|
639
|
|
|
sample: 3, |
|
640
|
|
|
partition: 3, |
|
641
|
|
|
groupBy: 3, |
|
642
|
|
|
countBy: 3, |
|
643
|
|
|
sortBy: 3, |
|
644
|
|
|
indexBy: 3, |
|
645
|
|
|
findIndex: 3, |
|
646
|
|
|
findLastIndex: 3 |
|
647
|
|
|
}; |
|
648
|
|
|
|
|
649
|
|
|
// Mix in each Underscore method as a proxy to `Collection#models`. |
|
650
|
|
|
addUnderscoreMethods(Collection, collectionMethods, 'models'); |
|
651
|
|
|
|
|
652
|
|
|
export { |
|
653
|
|
|
Collection |
|
654
|
|
|
}; |
|
655
|
|
|
|